package httpServer;

import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 
 * Class to handle the TCP connection between the server and one client.
 * 
 * This class expects and reads an HTTP request from the constructor's socket.
 * It then analyzes the request, handling requests that cannot be fulfilled by
 * returning an HTTP response with a descriptive status.  If the request may
 * be successfully fulfilled, the Response class is called. 
 * 
 * @author Rebecca Chandler, Scott Wendlandt, and Li Weihan
 *
 */
public class ServerClientConnection implements Runnable {

	// Socket connection between client and server
	private Socket socket;
	
	// Socket's input
	private InputStream inputStream;
	
	// Socket's output
	private DataOutputStream transmitStream;
	
	// determines whether the connection shall remain open
	private boolean persist;
	
	// Status of the HTTP request being processed.  Used in reponse header.
	// This value is updated before every StatusException is thrown.
	private Status status;

	// Time in seconds that the server will wait for an expected input before timing out
	private int secondsToTimeout = 15; //default value

	// Carriage return + new line
	private final String CRLF = "\r\n";
	
	// Establishes hash keys used to access a header value that does not have an identifying field name
	private final int methodKey = new String("Method").hashCode();
	private final int objectPath = new String("Object Path").hashCode();
	
	/**
	 * Exception class used to indicate when current operation should be terminated
	 * because the request cannot be fulfilled as expected.
	 * 
	 * While seemingly fitting, it does not reference a Status enum because all its functions are contained
	 * in this class.  It functions only as a break/alert when the variable Status is changed to indicate a
	 * problematic HTTP request.
	 *
	 */
	private class StatusException extends Exception{}

	/**
	 * Constructor
	 * @param s - Socket containing the TCP connection
	 * @throws IOException - socket could not be accessed
	 */
	public ServerClientConnection(Socket s) throws IOException{
		this.socket = s;
		this.inputStream = s.getInputStream();
		this.transmitStream = new DataOutputStream(s.getOutputStream());
	}
	
	/**
	 * Reads and responds to HTTP requests sent through the socket while the
	 * connection is indicated to be persistent by the requester.
	 */
	@Override
	public void run() {
		persist = true;

		// Loop read then respond while persist, or time out
		while(persist){
			try {
				readAndRespond();
			} catch (IOException e) {
				System.out.println("Error writing to socket stream");
				persist = false;
				e.printStackTrace();
			}
		}

		try {
			socket.close();
		} catch (IOException e) {
			System.out.println("Error closing socket.");
			e.printStackTrace();
		}
	}

	/**
	 * Reads an HTTP request from the inputStream and outputs an HTTP response through the transmit stream.
	 * Currently, this method only handles GET requests.  Requests of other types create a Not Implemented response.
	 * 
	 * Possible responses include: OK, Not Found, Not Implemented, HTTP Version Error, and Bad Request
	 * @throws IOException - problem accessing the socket's input or output stream
	 */
	private void readAndRespond() throws IOException{
		persist = false;

		// Resets the response status for responding to the incoming request
		this.status = Status.PENDING;

		// Will catch Status Exceptions
		try{
			// Read header out of the socket
			// May throw StatusException indicating a bad request or HTTP version problem
			HashMap<Integer, String> header = getHeader();


			// Determine the method command of the request
			// GET method:
			if(header.get(methodKey).equals("GET")){

				File file = new File(new java.io.File(".").getCanonicalPath() + "//resources" + header.get(objectPath).substring(1));

				// Test that the file is of the standard type
				// This includes checks such as exists, contains "?", etc as necessary
				if (file.isFile()){
					this.status = Status.OK;

					transmitStream.write(("HTTP/1.1 " + this.status + CRLF ).getBytes());

					// Call response methods
					Response defaultResponse = new Response(file);

					defaultResponse.send(transmitStream);
				} else {
					this.status = Status.NOT_FOUND;
					throw new StatusException();
				}
			} 
			else {
				this.status = Status.NOT_IMPLEMENTED;
				throw new StatusException();
			}

			if(header.get(new String ("Connection").hashCode()).equals("keep-alive")){
				// Had to disable persistent connection due to errors that we cannot currently fix
				//persist = true;
			}
		}catch(StatusException e){
			// Send back an error code to client
			transmitStream.write(("HTTP/1.1 " + this.status + CRLF ).getBytes());
			persist = false;
		}
	}

	/**
	 * Reads the header from the inputStream.  Stores the value in each header field in a HashMap.
	 * The fields are accessed through the hashCode representation of the field name.  (As determined
	 * by the haseCode() method in the String class)
	 * 
	 * Checks for and throws Status exceptions if the HTTP version is not 1.1 and if the request method is not recognized.
	 * Accepted request methods are: GET, HEAD, PUT, POST, DELETE, TRACE, CONNECT, and OPTIONS
	 * 
	 * @return HashMap - Map of the header field values.
	 * "Method" command and "Object Path" values are stored using the static class variable keys.
	 * 
	 * @throws StatusException - Occurs for two conditions: Status will be set to HTTP_VERSION or BAD_REQUEST
	 */
	private HashMap<Integer, String> getHeader() throws StatusException{
		// Method vars
		HashMap<Integer, String> headerFields = new HashMap<Integer, String>();
		String header = "";
		String nextLine;
		String[] split = null;

		nextLine = nextLine();
		header += nextLine;

		// The pattern describes line with four parts: a method command, a string object path, "HTTP", HTTP version
		// Matcher compares the first line read to the pattern
		Pattern headerMethod = Pattern.compile("^(GET|HEAD|PUT|POST|DELETE|TRACE|CONNECT|OPTIONS)" +
		"( \\S+ )(HTTP/)(.+)$");
		Matcher methodMatcher = headerMethod.matcher(nextLine);

		// If the line starts with an HTTP method command, then store that.  If not, throw "bad request" status error
		if(methodMatcher.matches()){
			// Throw "HTTP version" status error if the HTTP version is not 1.1
			if(!(methodMatcher.group(4).equals("1.1"))){
				this.status = Status.HTTP_VERSION;
				throw new StatusException();
				// If everything is OK, store the method command and object path in the header map
			} else {
				headerFields.put(methodKey, methodMatcher.group(1));
				headerFields.put(objectPath, methodMatcher.group(2));
			}
			// If the line doesn't match the pattern at all, then throw "bad request" status error
		} else {
			this.status = Status.BAD_REQUEST;
			throw new StatusException();
		}

		// Reads the lines of the header,
		// storing them in headerFields using the HashMap of field name (an int) as the key,
		// and a String of the field value as the value
		do {
			nextLine = nextLine();
			header += nextLine;

			split = nextLine.split(":\\s*", 2);
			if(split.length == 2){
				headerFields.put(split[0].hashCode(), split[1]);
			}
		} while (!nextLine.equals(""));

		// The header was read without problem.  Status changed from PENDING to OK.
		this.status = Status.OK;

		return headerFields;
	}


	// ---- HELPER METHODS FOR READING FROM INPUT STREAM ----

	/**
	 * Helper function for reading from SercerClientConnection's inputStream.
	 * 
	 * Reads chars from the byte stream until a CRLF is encountered.  Return value includes the CRLF after the string of chars
	 * 
	 * This function makes use of the next() helper method
	 * @throws StatusException - Timeout Status Exception when a byte has not been
	 * read by class var "secondsToTimeout"
	 */
	private String nextLine () throws StatusException{
		String returnString = "";
		char nextChar;
		boolean found = false;

		do{	
			nextChar = (char)nextByte();

			if(nextChar == '\r'){
				nextChar = nextChar();
				if(nextChar == '\n'){
					found = true;
				}else{
					returnString += "\r\n";
				}
			}else{
				returnString += nextChar;
			}
		}while(!found);

		return returnString;
	}
	
	/**
	 * Helper function for reading from SercerClientConnection's inputStream.
	 * 
	 * This helper function returns the next byte in this class's inputStream read as a char
	 * 
	 * It does not cause a timeout status exception.
	 */
	private char nextChar (){
		byte[] byteArray = new byte[1];
		int one = 0;

		while (one == 0){
			try {
				one = inputStream.read(byteArray);
			} catch (IOException e) {
				System.out.println("Error getting a byte.");
			}
		}

		return (char)byteArray[0];
	}

	/**
	 * Helper function for reading from SercerClientConnection's inputStream.
	 * 
	 * Reads the next "length" number of bytes and returns them as a String.
	 * If the class's "secondsToTimeout" seconds elapse and no bytes can be
	 * read, then a null String is returned.
	 * 
	 * This function makes use of the next() helper function
	 * @throws StatusException - Timeout Status Exception when a byte has not been
	 * read by class var "secondsToTimeout"
	 */
	private byte nextByte() throws StatusException{
		byte[] byteBuffer = new byte[1];
		int bytesRead = -1;
		int secondsElapsed = 0;

		try {
			bytesRead = inputStream.read(byteBuffer);
		} catch (IOException e) {
			System.out.println("Error getting some bytes.");
		}

		while(bytesRead == 0){			
			// If the previous iteration did not return any bytes, then wait one second before trying again
			try {
				Thread.sleep(1000);
				secondsElapsed++;

				bytesRead = inputStream.read(byteBuffer);
			} catch (InterruptedException e) {
				// This should not happen
				System.out.println("Server-Client connection has been interrupted.");
				e.printStackTrace();
			} catch (IOException e) {
				System.out.println("Error getting some bytes.");
				e.printStackTrace();
			}

			if(secondsElapsed > this.secondsToTimeout){
				this.status = Status.REQUEST_TIMEOUT;
				throw new StatusException();
			}

		}
		return byteBuffer[0];
	}
	
	/**
	 * Sets the number of seconds the ServerClientConnection will wait until a timeout occurs.
	 * @param sec - number of seconds
	 */
	public void setSecondsToTimeout(int sec){
		this.secondsToTimeout = sec;
	}
}



